package spark.network;

import spark.network.protocol.Base;
import spark.network.protocol.Response;

import java.net.InetSocketAddress;
import java.util.LinkedList;

import org.apache.log4j.Logger;
import spark.network.strategy.GenericStrategy;
import spark.network.strategy.IProtocolHandleStrategy;
import spark.utility.OutWrapper;

abstract class DemonHandler<ConnectionsType extends IConnections> implements ITimeAdjuster {
	private static Logger logger = Logger.getLogger(DemonHandler.class);

	protected Sender sender;
	protected ConnectionsType connections;
	private GenericStrategy genericStrategy = new GenericStrategy();

	protected DemonHandler(ConnectionsType connections, Sender sender) {
		this.connections = connections;
		this.sender = sender;
	}

	public void handle(Base packet, InetSocketAddress source) {
		handle(packet, source, genericStrategy);
	}

/*	public <PacketType extends Response> void handle(PacketType packet, InetSocketAddress source, IProtocolHandleStrategy<PacketType> strategy) {
		if(!connections.isValid(source, packet)) {
			return;
		}
		Base request = sender.updateAwaitingConfirmations(packet);
		if(request != null) {
			//noinspection unchecked
			packet.setRequest(request);
			handle(packet, strategy);
		}
		else {
			logger.info("Received a 'response' but no matching request. Discarding the response. Response: " + packet + " from " + source);
		}
	}*/

	public <PacketType extends Base> void handle(PacketType packet, InetSocketAddress source, IProtocolHandleStrategy<PacketType> strategy) {
		if(!connections.isValid(source, packet)) {
			return;
		}
		if(packet instanceof Response) {
			Response response = (Response) packet;
			Base request = sender.updateAwaitingConfirmations(response);
			if(request != null) {
				//noinspection unchecked
				response.setRequest(request);
				handle(packet, strategy);
			}
			else {
				logger.info("Received a 'response' but no matching request. Discarding the response. Response: " + packet + " from " + source);
			}
		}
		else {
			handle(packet, strategy);
		}
	}

	private <PacketType extends Base> void handle(PacketType packet, IProtocolHandleStrategy<PacketType> strategy) {
		NetworkConnection connection = connections.getConnection(packet.getSession());
		LinkedList<Base> additionalData = new LinkedList<Base>();
		OutWrapper<Response> acknowledgement = null;
		if(packet.isReliable()) {
			acknowledgement = new OutWrapper<Response>();
			handleReliable(packet, strategy, acknowledgement, additionalData, connection);
		}
		else {
			handleUnreliable(packet, strategy, additionalData);
		}
		sendResponses(acknowledgement, additionalData, connection);
	}

	private void sendResponses(OutWrapper<Response> acknowledgement, LinkedList<Base> additionalData, NetworkConnection connection) {
		if(acknowledgement != null) {
			sender.send(acknowledgement.value, connection);
			// if there is an acknowledgement the packet we are responding to must be reliable so we buffer or
			// re-buffer (if we are responding for the 2nd+ time) the response
			connection.bufferResponse(acknowledgement.value);
		}
		for(Base packet : additionalData) {
			sender.send(packet, connection);
		}
	}

	private <PacketType extends Base> void handleUnreliable(PacketType packet, IProtocolHandleStrategy<PacketType> strategy, LinkedList<Base> additionalData) {
		strategy.handle(packet, null, additionalData);
	}

	private <PacketType extends Base> void handleReliable(PacketType packet, IProtocolHandleStrategy<PacketType> strategy, OutWrapper<Response> acknowledgement, LinkedList<Base> additionalData, NetworkConnection connection) {
		// we handle a reliable packet, this means that it is possible we received an equal packet before but our
		// response got lost on the way (udp) and the client restated its request (resend the packet), we figure
		// out if this is the case by checking if we have a response buffered for this packet
		BufferedPacket<Response> bufferedResponse = connection.getBufferedResponse(packet);
		// if there is no response for this packet we received it for the first time and handle it accordingly
		if(bufferedResponse == null) {
			strategy.handle(packet, acknowledgement, additionalData);
			if(acknowledgement.value == null) {
				logger.warn("Strategy " + strategy.getClass() + "\n" +
				             "did not acknowledge reliable packet " + packet.getClass());
			}
			else if(!acknowledgement.value.getRequestTypeClass().isInstance(packet)) {
				logger.warn("Strategy " + strategy.getClass() + "\n" +
				             "acknowledged reliable packet with " + acknowledgement.value.getClass() + "\n" +
				             "which is a response to " +  acknowledgement.value.getRequestTypeClass() + "\n" +
				             "but the request is of class " + packet.getClass());
			}
		}
		// the response might have gotten lost or the client restated its request before it arrived, whatever the reason
		// we simply send the same response again
		else {
			// it is possible that the client queried us more than once for a response and we received the x+n query
			// before the x and already responded to it, now we get the x query (the outdated, earlier one)
			// so we only want to actually send the response if the query we received is a newer one than the one
			// we have responded to already
			if(packet.getTimeStampSend() > bufferedResponse.getPacket().getTimeStampSend()) {
				bufferedResponse.getPacket().setTimeStampSend(packet.getTimeStampSend());
				acknowledgement.value = bufferedResponse.getPacket();
			}
		}
	}

	@Override
	public long getTimeAdjustment() {
		return sender.getTimeAdjustment();
	}

	@Override
	public void setTimeAdjustment(long timeAdjustment) {
		sender.setTimeAdjustment(timeAdjustment);
	}
}
